// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <stdint.h>
#include <utility>

#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/macros.h"
#include "base/memory/ptr_util.h"
#include "base/run_loop.h"
#include "base/test/test_simple_task_runner.h"
#include "base/threading/thread.h"
#include "content/browser/indexed_db/indexed_db_connection.h"
#include "content/browser/indexed_db/indexed_db_context_impl.h"
#include "content/browser/indexed_db/indexed_db_factory_impl.h"
#include "content/browser/indexed_db/mock_indexed_db_callbacks.h"
#include "content/browser/indexed_db/mock_indexed_db_database_callbacks.h"
#include "content/browser/quota/mock_quota_manager_proxy.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/mock_special_storage_policy.h"
#include "content/public/test/test_browser_context.h"
#include "content/public/test/test_browser_thread_bundle.h"
#include "storage/browser/quota/quota_manager.h"
#include "storage/browser/quota/special_storage_policy.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/origin.h"

using url::Origin;

namespace content {

class IndexedDBTest : public testing::Test {
public:
    const Origin kNormalOrigin;
    const Origin kSessionOnlyOrigin;

    IndexedDBTest()
        : kNormalOrigin(GURL("http://normal/"))
        , kSessionOnlyOrigin(GURL("http://session-only/"))
        , task_runner_(new base::TestSimpleTaskRunner)
        , special_storage_policy_(new MockSpecialStoragePolicy)
        , quota_manager_proxy_(new MockQuotaManagerProxy(nullptr, nullptr))
    {
        special_storage_policy_->AddSessionOnly(kSessionOnlyOrigin.GetURL());
    }
    ~IndexedDBTest() override
    {
        quota_manager_proxy_->SimulateQuotaManagerDestroyed();
    }

protected:
    void FlushIndexedDBTaskRunner() { task_runner_->RunUntilIdle(); }

    scoped_refptr<base::TestSimpleTaskRunner> task_runner_;
    scoped_refptr<MockSpecialStoragePolicy> special_storage_policy_;
    scoped_refptr<MockQuotaManagerProxy> quota_manager_proxy_;

private:
    TestBrowserThreadBundle thread_bundle_;

    DISALLOW_COPY_AND_ASSIGN(IndexedDBTest);
};

TEST_F(IndexedDBTest, ClearSessionOnlyDatabases)
{
    base::ScopedTempDir temp_dir;
    ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

    base::FilePath normal_path;
    base::FilePath session_only_path;

    // Create the scope which will ensure we run the destructor of the context
    // which should trigger the clean up.
    {
        scoped_refptr<IndexedDBContextImpl> idb_context = new IndexedDBContextImpl(
            temp_dir.GetPath(), special_storage_policy_.get(),
            quota_manager_proxy_.get(), task_runner_.get());

        normal_path = idb_context->GetFilePathForTesting(kNormalOrigin);
        session_only_path = idb_context->GetFilePathForTesting(kSessionOnlyOrigin);
        ASSERT_TRUE(base::CreateDirectory(normal_path));
        ASSERT_TRUE(base::CreateDirectory(session_only_path));
        FlushIndexedDBTaskRunner();
        base::RunLoop().RunUntilIdle();
        quota_manager_proxy_->SimulateQuotaManagerDestroyed();
    }

    FlushIndexedDBTaskRunner();
    base::RunLoop().RunUntilIdle();

    EXPECT_TRUE(base::DirectoryExists(normal_path));
    EXPECT_FALSE(base::DirectoryExists(session_only_path));
}

TEST_F(IndexedDBTest, SetForceKeepSessionState)
{
    base::ScopedTempDir temp_dir;
    ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

    base::FilePath normal_path;
    base::FilePath session_only_path;

    // Create the scope which will ensure we run the destructor of the context.
    {
        // Create some indexedDB paths.
        // With the levelDB backend, these are directories.
        scoped_refptr<IndexedDBContextImpl> idb_context = new IndexedDBContextImpl(
            temp_dir.GetPath(), special_storage_policy_.get(),
            quota_manager_proxy_.get(), task_runner_.get());

        // Save session state. This should bypass the destruction-time deletion.
        idb_context->SetForceKeepSessionState();

        normal_path = idb_context->GetFilePathForTesting(kNormalOrigin);
        session_only_path = idb_context->GetFilePathForTesting(kSessionOnlyOrigin);
        ASSERT_TRUE(base::CreateDirectory(normal_path));
        ASSERT_TRUE(base::CreateDirectory(session_only_path));
        base::RunLoop().RunUntilIdle();
    }

    // Make sure we wait until the destructor has run.
    base::RunLoop().RunUntilIdle();

    // No data was cleared because of SetForceKeepSessionState.
    EXPECT_TRUE(base::DirectoryExists(normal_path));
    EXPECT_TRUE(base::DirectoryExists(session_only_path));
}

class ForceCloseDBCallbacks : public IndexedDBCallbacks {
public:
    ForceCloseDBCallbacks(scoped_refptr<IndexedDBContextImpl> idb_context,
        const Origin& origin)
        : IndexedDBCallbacks(nullptr, origin, nullptr)
        , idb_context_(idb_context)
        , origin_(origin)
    {
    }

    void OnSuccess() override { }
    void OnSuccess(const std::vector<base::string16>&) override { }
    void OnSuccess(std::unique_ptr<IndexedDBConnection> connection,
        const IndexedDBDatabaseMetadata& metadata) override
    {
        connection_ = std::move(connection);
        idb_context_->ConnectionOpened(origin_, connection_.get());
    }

    IndexedDBConnection* connection() { return connection_.get(); }

protected:
    ~ForceCloseDBCallbacks() override { }

private:
    scoped_refptr<IndexedDBContextImpl> idb_context_;
    Origin origin_;
    std::unique_ptr<IndexedDBConnection> connection_;
    DISALLOW_COPY_AND_ASSIGN(ForceCloseDBCallbacks);
};

TEST_F(IndexedDBTest, ForceCloseOpenDatabasesOnDelete)
{
    base::ScopedTempDir temp_dir;
    ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

    scoped_refptr<MockIndexedDBDatabaseCallbacks> open_db_callbacks(
        new MockIndexedDBDatabaseCallbacks());
    scoped_refptr<MockIndexedDBDatabaseCallbacks> closed_db_callbacks(
        new MockIndexedDBDatabaseCallbacks());

    base::FilePath test_path;

    // Create the scope which will ensure we run the destructor of the context.
    {
        TestBrowserContext browser_context;

        const Origin kTestOrigin(GURL("http://test/"));

        scoped_refptr<IndexedDBContextImpl> idb_context = new IndexedDBContextImpl(
            temp_dir.GetPath(), special_storage_policy_.get(),
            quota_manager_proxy_.get(), task_runner_.get());

        scoped_refptr<ForceCloseDBCallbacks> open_callbacks = new ForceCloseDBCallbacks(idb_context, kTestOrigin);

        scoped_refptr<ForceCloseDBCallbacks> closed_callbacks = new ForceCloseDBCallbacks(idb_context, kTestOrigin);

        IndexedDBFactory* factory = idb_context->GetIDBFactory();

        test_path = idb_context->GetFilePathForTesting(kTestOrigin);

        std::unique_ptr<IndexedDBPendingConnection> open_connection(
            base::MakeUnique<IndexedDBPendingConnection>(
                open_callbacks, open_db_callbacks, 0 /* child_process_id */,
                0 /* host_transaction_id */, 0 /* version */));
        factory->Open(base::ASCIIToUTF16("opendb"), std::move(open_connection),
            nullptr /* request_context */, Origin(kTestOrigin),
            idb_context->data_path());
        std::unique_ptr<IndexedDBPendingConnection> closed_connection(
            base::MakeUnique<IndexedDBPendingConnection>(
                closed_callbacks, closed_db_callbacks, 0 /* child_process_id */,
                0 /* host_transaction_id */, 0 /* version */));
        factory->Open(base::ASCIIToUTF16("closeddb"), std::move(closed_connection),
            nullptr /* request_context */, Origin(kTestOrigin),
            idb_context->data_path());

        closed_callbacks->connection()->Close();

        // TODO(jsbell): Remove static_cast<> when overloads are eliminated.
        idb_context->TaskRunner()->PostTask(
            FROM_HERE,
            base::Bind(static_cast<void (IndexedDBContextImpl::*)(const Origin&)>(
                           &IndexedDBContextImpl::DeleteForOrigin),
                idb_context, kTestOrigin));
        FlushIndexedDBTaskRunner();
        base::RunLoop().RunUntilIdle();
    }

    // Make sure we wait until the destructor has run.
    base::RunLoop().RunUntilIdle();

    EXPECT_TRUE(open_db_callbacks->forced_close_called());
    EXPECT_FALSE(closed_db_callbacks->forced_close_called());
    EXPECT_FALSE(base::DirectoryExists(test_path));
}

TEST_F(IndexedDBTest, DeleteFailsIfDirectoryLocked)
{
    base::ScopedTempDir temp_dir;
    ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
    const Origin kTestOrigin(GURL("http://test/"));

    scoped_refptr<IndexedDBContextImpl> idb_context = new IndexedDBContextImpl(
        temp_dir.GetPath(), special_storage_policy_.get(),
        quota_manager_proxy_.get(), task_runner_.get());

    base::FilePath test_path = idb_context->GetFilePathForTesting(kTestOrigin);
    ASSERT_TRUE(base::CreateDirectory(test_path));

    std::unique_ptr<LevelDBLock> lock = LevelDBDatabase::LockForTesting(test_path);
    ASSERT_TRUE(lock);

    // TODO(jsbell): Remove static_cast<> when overloads are eliminated.
    void (IndexedDBContextImpl::*delete_for_origin)(const Origin&) = &IndexedDBContextImpl::DeleteForOrigin;
    idb_context->TaskRunner()->PostTask(
        FROM_HERE,
        base::Bind(delete_for_origin, idb_context, kTestOrigin));
    FlushIndexedDBTaskRunner();

    EXPECT_TRUE(base::DirectoryExists(test_path));
}

TEST_F(IndexedDBTest, ForceCloseOpenDatabasesOnCommitFailure)
{
    const Origin kTestOrigin(GURL("http://test/"));

    base::ScopedTempDir temp_dir;
    ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

    scoped_refptr<IndexedDBContextImpl> context = new IndexedDBContextImpl(
        temp_dir.GetPath(), special_storage_policy_.get(),
        quota_manager_proxy_.get(), task_runner_.get());

    scoped_refptr<IndexedDBFactoryImpl> factory = static_cast<IndexedDBFactoryImpl*>(context->GetIDBFactory());

    scoped_refptr<MockIndexedDBCallbacks> callbacks(new MockIndexedDBCallbacks());
    scoped_refptr<MockIndexedDBDatabaseCallbacks> db_callbacks(
        new MockIndexedDBDatabaseCallbacks());
    const int64_t transaction_id = 1;
    std::unique_ptr<IndexedDBPendingConnection> connection(
        base::MakeUnique<IndexedDBPendingConnection>(
            callbacks, db_callbacks, 0 /* child_process_id */, transaction_id,
            IndexedDBDatabaseMetadata::DEFAULT_VERSION));
    factory->Open(base::ASCIIToUTF16("db"), std::move(connection),
        nullptr /* request_context */, Origin(kTestOrigin),
        temp_dir.GetPath());

    EXPECT_TRUE(callbacks->connection());

    // ConnectionOpened() is usually called by the dispatcher.
    context->ConnectionOpened(kTestOrigin, callbacks->connection());

    EXPECT_TRUE(factory->IsBackingStoreOpen(kTestOrigin));

    // Simulate the write failure.
    leveldb::Status status = leveldb::Status::IOError("Simulated failure");
    context->GetIDBFactory()->HandleBackingStoreFailure(kTestOrigin);

    EXPECT_TRUE(db_callbacks->forced_close_called());
    EXPECT_FALSE(factory->IsBackingStoreOpen(kTestOrigin));
}

} // namespace content
